package mya_dc.shared_classes;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedList;

import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

/**
 * A Tree-like representation of files and directories.
 * 
 * @author      Adam Levi
 * <br>			MYA
 */
public class MYA_FileTreeNode implements TreeModel, Serializable
{
	private static final long serialVersionUID = 709998496123529891L;

	/**
	 * a function that checks if the serverTree has links that doesn't appear
	 * in clientTree. This method exists for synchronization between the project
	 * at the client side, and the project at the server side. 
	 *
	 * @param 	clientTree - The tree at the client side.
	 * @param	serverTree - The tree at the server side.
	 * 
	 * @return	a Linked-List of type Object[2], the first object in each node
	 * is a Strings, each string represents a path to a	file/directory
	 * that was deleted. the second object is a boolean that is true if and only
	 * if the original path was related to a file (and not a directory).
	 */
	public static LinkedList<Object[]> removeDeadFiles(
			MYA_FileTreeNode clientTree,
			MYA_FileTreeNode serverTree)
	{
		LinkedList<Object[]> retList = new LinkedList<Object[]>();
		LinkedList<MYA_FileTreeNode> deadFiles = getDeadFiles(clientTree,serverTree);
		for(MYA_FileTreeNode file : deadFiles)
		{
			Object[] pathAndBool = new Object[2];
			pathAndBool[0] = new String(file.m_File.getPath());
			pathAndBool[1] = file.m_File.isFile();
			retList.add(pathAndBool);
			mya_dc.shared_classes.files_manipulation.FileObject.deleteDirectory(file.m_File);
		}
		return retList;
	}
	
	/**
	 * a function that checks if the serverTree has links that doesn't appear
	 * in clientTree. if will return a list of nodes that should be removed from
	 * the checkedTree.
	 *
	 * @param 	sourceTree 	- The tree that possess the 'true' values.
	 * @param	checkedTree - The tree that is being checked against SourceTree.
	 * 
	 * @return	a linked-list containing all the nodes in checkedTree that should
	 * be removed
	 */
	public static LinkedList<MYA_FileTreeNode> getDeadFiles(
			MYA_FileTreeNode sourceTree,
			MYA_FileTreeNode checkedTree)
	{
		//this function is called from outside with the root node
		//the root node doesn't need to be checked.
		LinkedList<MYA_FileTreeNode> list = new LinkedList<MYA_FileTreeNode>();
		for (MYA_FileTreeNode child : checkedTree.m_Children)
		{
			list.addAll(getDeadFilesAux(sourceTree,checkedTree,child));
		}
		return list;
	}
	
	/**
	 * an auxiliary function to use with getDeadFiles()
	 *
	 * @param 	sourceTree 	- The tree that possess the 'true' values.
	 * @param	checkedTree - The tree that is being checked against SourceTree.
	 * @param	curCheckedNode - The current node that is being verified.
	 * 
	 * @return	a linked-list containing all the nodes in curCheckedNode that should
	 * be removed
	 * 
	 * @see		getDeadFiles()
	 * 
	 */
	private static LinkedList<MYA_FileTreeNode> getDeadFilesAux(
			MYA_FileTreeNode sourceTree,
			MYA_FileTreeNode checkedTree,
			MYA_FileTreeNode curCheckedNode
			)
	{
		LinkedList<MYA_FileTreeNode> list = new LinkedList<MYA_FileTreeNode>();
		//check given curCheckedNode
		if (!(MYA_FileTreeNode.isNodeExistsInSameRelativePath
				(curCheckedNode,checkedTree,sourceTree)))
		{
			list.add(curCheckedNode);
			return list;
		}
		for (MYA_FileTreeNode child : curCheckedNode.m_Children)
		{
			//- recursively enters to children
			list.addAll(getDeadFilesAux(sourceTree,checkedTree,child));
		}
		return list;
	}
	
	/**
	 * checks if both tree possess a given node in the same relative path
	 * (relative means - the path from the root of the treE)
	 *
	 * @param 	node 		- a node to check
	 * @param	TreeOneRoot - a MYA_FileTreeNode that the given node <b> exists </b> in it.
	 * @param	TreeTwoRoot - a MYA_FileTreeNode.
	 * 
	 * @return	true if both trees have 'node' in the same relative path, otherwise false.
	 * 
	 */
	private static boolean isNodeExistsInSameRelativePath(
			MYA_FileTreeNode node,
			MYA_FileTreeNode TreeOneRoot, //assuming node exists in TreeOne
			MYA_FileTreeNode TreeTwoRoot)
	{
		LinkedList<MYA_FileTreeNode> path = MYA_FileTreeNode.getPathFromAncestor(TreeOneRoot,node);
		MYA_FileTreeNode curNode = TreeTwoRoot;
		boolean found = false;
		while(path.size()>0)
		{
			found = false;
			for (MYA_FileTreeNode child : curNode.m_Children)
			{
				if (child.equalByNamesOnly(path.getFirst()))
				{
					// this is the next item in 'path'
					curNode = child;
					found = true;
					path.removeFirst();
					break;
				}
			}
			if (!found)
				return false;
		}
		return true;
	}
	
	/**
	 * creates a path representation from 'ancestor' to 'node'
	 *
	 * @param 	ancestor 	- a node that <b>must</b> be the ancestor-node.
	 * @param	node		- a child (not just direct children of ancestor) node.
	 *  
	 * @return	a linked-list that represents the pass from ancestor to node
	 * 
	 */
	private static LinkedList<MYA_FileTreeNode> getPathFromAncestor(
			MYA_FileTreeNode ancestor,
			MYA_FileTreeNode node)
	{
		LinkedList<MYA_FileTreeNode> path = new LinkedList<MYA_FileTreeNode>();
		MYA_FileTreeNode curNode = node;
		while(curNode!=null && !curNode.equals(ancestor))
		{
			path.addFirst(curNode);
			curNode = curNode.m_Parent;

		}
		return path;
	}
	
	/**
	 * checks if the given node has the filename has this node.
	 *
	 * @param 	node 		- a node to check
	 *  
	 * @return	true if both nodes have the same filename, otherwise false.
	 * 
	 */
	private boolean equalByNamesOnly(MYA_FileTreeNode node)
	{
		return (this.m_File.getName().equals(node.m_File.getName()));
	}
	
	public enum FilesToGet {OUT_DATED_FILES, ALL_FILES} ;
	
	/**
	 * Constructor
	 *
	 * @param 	file 	- the file of the node.
	 * @param	parent	- the parent node.
	 *  
	 */
	public MYA_FileTreeNode(MYA_File file, MYA_FileTreeNode parent)
	{
		m_Parent = parent;
		m_File = file;
		m_Children = new ArrayList<MYA_FileTreeNode>();
		updateTree();
	}
	
	/**
	 * @return	The node's file.
	 */
	public MYA_File getFile()
	{
		return m_File;
	}
	
	/**
	 * @return	The node's children.
	 */
	public ArrayList<MYA_FileTreeNode> getChildren()
	{
		return m_Children;
	}
	
	/**
	 * @return	The node's number of children.
	 */
	public int directChildrenNumber()
	{
		return m_Children.size();
	}
	
	/**
	 * Updates the given node recursively. The function will check if there
	 * are any new children and will add them, and it will remove nodes that
	 * does not exist any more.
	 *<br><br> note that nodes that exist will not changed at all.
	 *<br>this function should be called with care since it could require 
	 *a long time to complete.
	 * 
	 */
	public void updateTree()
	{
		if (m_File.isFile())
			return;
		MYA_File[] curChildrenArray = m_File.listFiles();
		//building hash table from curChildrenArray
		ArrayList<MYA_File> curChildrenAL = new ArrayList<MYA_File>(curChildrenArray.length);
		for (MYA_File file : curChildrenArray)
			curChildrenAL.add(file);
		LinkedList<MYA_File> linksToRemove = new LinkedList<MYA_File>();
		//updating existing nodes and marking nodes to remove
		for (MYA_FileTreeNode fileTreeNode : m_Children)
		{
			if (curChildrenAL.contains(fileTreeNode.m_File))
			{
				fileTreeNode.updateTree();
			}
			else //need to remove
			{
				linksToRemove.add(fileTreeNode.m_File);
			}
		}
		// create dummy MYA_FileTreeNode
		MYA_FileTreeNode dummyNode = new MYA_FileTreeNode();
		
		//removing dead links
		for (MYA_File file : linksToRemove)
		{
			dummyNode.m_File = file;
			m_Children.remove(dummyNode);
		}
		//adding new nodes
		for (MYA_File file : curChildrenArray)
		{
			dummyNode.m_File = file;
			//dummyNode.m_Parent = this;
			if (m_Children.contains(dummyNode))
				continue;
			MYA_FileTreeNode tmpNode = new MYA_FileTreeNode(file,this);
			m_Children.add(tmpNode);
		}
	}
	
	/**
	 * Note: this function works in O(size of tree) and not O(1)
	 * 
	 * @return the Total size of all the files in the tree.
	 *  
	 */
	public long size()
	{
		if (m_File.isFile())
			return m_File.length();
		long sum = 0;
		for (MYA_FileTreeNode child : m_Children)
			sum += child.size();
		return sum;
	}
	
	/**
	 * @param typeToGet - The type of files to get.
	 * 
	 * @return a list of files of type typeToGet
	 */
	public LinkedList<MYA_File> getFiles(FilesToGet typeToGet)
	{
		LinkedList<MYA_File> retList = new LinkedList<MYA_File>();
		getFilesAux(retList,typeToGet);
		return retList;
		
	}
	
	public void markTreeAsNotSent()
	{
		if (m_File.isFile())
		{
			m_File.setNeedTransmittion();
			return;
		}
		for (MYA_FileTreeNode child : m_Children)
			child.markTreeAsNotSent();
	}
	
	/**
	 * marks each node in tree as "SENT TO SERVER"
	 */
	public void markTreeAsSent() //
	{
		if (m_File.isFile())
		{
			m_File.setLastTransmitted();
			return;
		}
		for (MYA_FileTreeNode child : m_Children)
				child.markTreeAsSent();
	}
	
	/**
	 * @return The node's filename
	 */
	@Override public String toString()
	{
		return m_File.getName();
	}
	
	/**
	 * Check if the given object is equal to the current object.
	 * <br> obj is equal to 'this' if its of type MYA_FileTreeNode and
	 * it's file is equal to 'this'-file, or obj is of type MYA_File and
	 * it is equal to 'this'-file.
	 * @return true if the obj is equal to this, otherwise - false.
	 */
	@Override public boolean equals(Object obj)
	{
		if (obj instanceof MYA_FileTreeNode)
		{
			MYA_FileTreeNode temp = (MYA_FileTreeNode)obj;
			return (this.getFile().equals(temp.getFile()));
		}
		else /* object of type MYA_File */
		{
			return (this.getFile().equals(obj));
		}
	}
	
	@Override public int hashCode()
	{
		return m_File.hashCode();
	}; 
	
	
	/**
	 * a function to retrieve the child at the given path, note that
	 * the path should not contain the root of the project
	 * 
	 * @param path - a representation of a path
	 * 
	 * @return the MYA_File of the given file if a the path is valid, otherwise null
	 */
	public MYA_File getChild(String[] path)
	{
		//path 0 -> a directory/path UNDER the root
		MYA_FileTreeNode curNode = (MYA_FileTreeNode)this.getRoot();
		for (int i = 0; i < path.length; i++)
		{
			boolean found = false;
			for (MYA_FileTreeNode child : curNode.m_Children)
			{
				String curName = child.getFile().getName();
				if (curName.equals(path[i]))
				{
					curNode = child;
					found = true;
					break;
				}
			}
			if (!found)
				return null;
			
		}
		return curNode.m_File;
	}
	
	/**
	 * a function to retrieve the child at the given path
	 * 
	 * @param path - a TreePath
	 * 
	 * @return the node at the given path, or null if no such node exists.
	 */
	public MYA_FileTreeNode getChild(TreePath path)
	{
		MYA_FileTreeNode curNode = this;
		if (curNode.equals(path.getPathComponent(0))==false)
			return null;
		int n = path.getPathCount();
		int i = 1;
		for (; i < n; i++)
		{
			boolean matchedChild = false;
			for (MYA_FileTreeNode child : curNode.m_Children)
			{
				if (child.equals(path.getPathComponent(i)))
				{
					// match found
					curNode = child;
					matchedChild = true;
					break;
				}
			}
			if (matchedChild)
				continue;
			else
				return null;		
		}
		if (i == n) //matched all
			return curNode;
		return null;
	}
	
	/**
	 * a private constructor
	 */	
	private MYA_FileTreeNode()
	{
	}
	
	/**
	 * an auxiliary function to use with getFiles.
	 * 
	 * @param filesList - a linked-list to put the files into
	 * @param typeToGet - the type of files to get
	 * 
	 */	
	private void getFilesAux(LinkedList<MYA_File> filesList, FilesToGet typeToGet)
	{
		if (m_File.isFile())
		{
			switch (typeToGet)
			{
			case ALL_FILES:
				{
					filesList.add(m_File);
					break;
				}
			case OUT_DATED_FILES:
				{
					if (m_File.needToTransmit())
						filesList.add(m_File);
					break;
				}
			}
			return;
		}
		for (MYA_FileTreeNode child : m_Children)
			child.getFilesAux(filesList,typeToGet);
	}
	
	/**
	 * @return The root of the tree
	 */	
	private MYA_FileTreeNode getTreeRoot()
	{
		MYA_FileTreeNode curNode = this;
		while(m_Parent != null)
		{
			curNode = curNode.m_Parent;
		}
		return curNode;
			
	}
	
	//variables
	private MYA_File 					m_File;
	private MYA_FileTreeNode 			m_Parent;
	private ArrayList<MYA_FileTreeNode> m_Children;
	
	//tree model implementation
	@Override
	public void addTreeModelListener(TreeModelListener l){}
	
	private synchronized Object getChildSync(Object parent, int index) {
		MYA_FileTreeNode par = (MYA_FileTreeNode)parent;
		if (index < 0 || index >= par.m_Children.size())
			return null;
		return par.m_Children.get(index);
	}
	
	@Override
	public Object getChild(Object parent, int index) {
		return getChildSync(parent,index);
	}
	
	private synchronized int getChildCountSync(Object parent) {
		return ((MYA_FileTreeNode)parent).m_Children.size();
	}
	@Override
	public int getChildCount(Object parent) {
		return getChildCountSync(parent);
	}
	
	private synchronized int getIndexOfChildSync(Object parent, Object child) {
		return ((MYA_FileTreeNode)parent).m_Children.indexOf(child);
	}
	@Override
	public int getIndexOfChild(Object parent, Object child) {
		return getIndexOfChildSync(parent,child);
	}
	private synchronized Object getRootSync() {
		return getTreeRoot();
	}
	
	@Override
	public Object getRoot() {
		return getRootSync();
	}
	
	private synchronized boolean isLeafSync(Object node) {
		return ((MYA_FileTreeNode)node).m_File.isFile();
	}

	@Override
	public boolean isLeaf(Object node) {
		return isLeafSync(node);
	}
	@Override
	public void removeTreeModelListener(TreeModelListener l) {}
	public void valueForPathChanged(TreePath path, Object newValue) { }

}
